package ar.uba.fi.tonyvaliente.files;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;

/**
 * Clase que representa el header de un archivo de bloques.
 * Guarda el tamanio de los bloques del archivo, la cantidad de bloques
 * en el mismo, y un conjunto de propiedades (pares clave-valor).
 * La cantidad de propiedades que se pueden guardar esta limitada por el
 * tamanio en disco del header, que esta dado por la constante SIZE_IN_BYTES.
 * La clase posee metodos para serializar (crear un byte array con la info 
 * del header) y deserializar (setear a un objeto FileHeader los valores leidos
 * de un byte array).
 */
public class FileHeader {
	
	private int blockSize;
	private int blockCount;
	private float maxBlockLoadFactorInsert;
	private HashMap<String, String> properties;
	private int dataLength;
	
	public static final int SIZE_IN_BYTES = 4096;
	public static final float MAX_HEADER_LOAD_FACTOR_FOR_INSERT = 0.8f;
	
	/**
	 * Crea un nuevo objeto FileHeader con el tamanio de bloque
	 * recibido por parametro 
	 * @param blockSize tamanio de bloques
	 */
	public FileHeader(int blockSize, float maxBlockLoadFactorInsert) {
		this.blockSize = blockSize;
		this.blockCount = 0;
		this.maxBlockLoadFactorInsert = maxBlockLoadFactorInsert;
		this.properties = new HashMap<String,String>();
		this.dataLength = 16;
	}

	/**
	 * Devuelve el tamanio de bloques guardado en el objeto 
	 * @return el tamanio de bloques guardado en el objeto
	 */
	public int getBlockSize() {
		return blockSize;
	}

	/**
	 * Asigna al objeto un nuevo tamanio de bloques
	 * @param blockSize nuevo tamanio de bloques a setear al objeto
	 */
	public void setBlockSize(int blockSize) {
		this.blockSize = blockSize;
	}

	/**
	 * Devuelve la cantidad de bloques guardada en el objeto 
	 * @return la cantidad de bloques guardada en el objeto
	 */
	public int getBlockCount() {
		return blockCount;
	}

	/**
	 * Asigna al objeto una nueva cantidad de bloques
	 * @param blockSize nueva cantidad de bloques a setear al objeto
	 */
	public void setBlockCount(int blockCount) {
		this.blockCount = blockCount;
	}
	
	public float getMaxBlockLoadFactorInsert() {
		return maxBlockLoadFactorInsert;
	}

	public void setMaxBlockLoadFactorInsert(float maxBlockLoadFactorInsert) {
		this.maxBlockLoadFactorInsert = maxBlockLoadFactorInsert;
	}

	/**
	 * Devuelve el valor de la propiedad cuyo nombre recibe por parametro
	 * Si la misma no existe, devuelve null
	 * @param name nombre de la propiedad cuyo valor se quiere recuperar
	 * @return valor de la propiedad, o null si la misma no existe
	 */
	public String getProperty(String name) {
		return properties.get(name);
	}
	
	/**
	 * Agrega una propiedad nueva al objeto, o modifica el valor de una ya
	 * existente. En ambos casos verifica que el tamanio serializado del header
	 * no supere un cierto tamanio, dado por SIZE_IN_BYTES en la modificacion
	 * y por SIZE_IN_BYTES * MAX_LOAD_FACTOR_FOR_INSERT al agregar.
	 * 
	 * @return true Si puede agregar/modificar satisfactoriamente, false en
	 * caso contrario
	 */
	public boolean setProperty(String name, String value) {
		boolean contains = properties.containsKey(name);
		if (contains) {
			int newPropertySize = calculatePropertySize(name,value);
			int oldPropertySize = calculatePropertySize(name, properties.get(name));
			if (dataLength + newPropertySize - oldPropertySize <= SIZE_IN_BYTES) {
				properties.put(name, value);
				dataLength += newPropertySize - oldPropertySize;
				return true;
			} else {
				return false;
			}
		} else {
			int propertySize = calculatePropertySize(name,value);
			if (dataLength + propertySize <= (int)(SIZE_IN_BYTES * MAX_HEADER_LOAD_FACTOR_FOR_INSERT)) {
				properties.put(name, value);
				dataLength += propertySize;
				return true;
			} else {
				return false;
			}
		}
	}
	
	/**
	 * Crea un array de bytes de tamanio SIZE_IN_BYTES con la informacion
	 * del header. 
	 * @return array de bytes de tamanio SIZE_IN_BYTES con la informacion
	 * del header
	 */
	public byte[] toByteArray() {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		DataOutputStream out = new DataOutputStream(baos);
		try {
			// Escribo tamanio de bloque, cantidad de bloques
			// y cantidad de properties
			out.writeInt(blockSize);
			out.writeInt(blockCount);
			out.writeFloat(maxBlockLoadFactorInsert);
			out.writeInt(properties.size());
			
			// Escribo las properties
			Iterator<String> keys = properties.keySet().iterator();
			while (keys.hasNext()) {
				String name = keys.next();
				String value = properties.get(name);
				out.writeInt(name.length());
				out.writeBytes(name);
				out.writeInt(value.length());
				out.writeBytes(value);
			}
			
			out.close();
			
			// Completo con ceros hasta alcanzar el tamanio SIZE_IN_BYTES
			byte[] bytes = baos.toByteArray();
			if (bytes.length < SIZE_IN_BYTES) {
				byte[] ceros = new byte[SIZE_IN_BYTES - bytes.length];
				Arrays.fill(ceros, (byte)0);
				
				baos = new ByteArrayOutputStream();
				out = new DataOutputStream(baos);
				out.write(bytes);
				out.write(ceros);
				out.close();
				
				bytes = baos.toByteArray();
			} else if (bytes.length > SIZE_IN_BYTES) {
				// TODO: remover esta verificacion, es solo para debug
				throw new RuntimeException("Header serializado de tamanio mayor al correcto");
			}
			
			// Devuelvo el array final
			return bytes;
			
		} catch (IOException e) {
			throw new RuntimeException("Error al serializar FileHeader", e);
		}
	}
	
	/**
	 * Crea un nuevo FileHeader con la informacion contenida en un array de
	 * bytes recibido por parametro. Si dicha informacion no es valida lanza una
	 * InvalidHeaderException 
	 * @param bytes array de bytes con la informacion serializada del header
	 * @return nuevo FileHeader con la informacion contenida en un array de bytes
	 * @throws InvalidHeaderException si la informacion del array de bytes
	 * no es valida
	 */
	public static FileHeader fromByteArray(byte[] bytes) throws InvalidHeaderException {
		if (bytes.length != SIZE_IN_BYTES) {
			throw new InvalidHeaderException("Byte array de tamanio incorrecto"
					 + " a la hora de crear FileHeader");
		}
		
		FileHeader header;
		
		ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
		DataInputStream in = new DataInputStream(bais);
		
		int blockSize;
		int blockCount;
		float maxBlockLoadFactorInsert;
		
		try {
			blockSize = in.readInt();
			blockCount = in.readInt();
			maxBlockLoadFactorInsert = in.readFloat();
			
			header = new FileHeader(blockSize, maxBlockLoadFactorInsert);
			header.setBlockCount(blockCount);
			
			int propertyCount = in.readInt();
			
			int bytesRead;
			for (int i = 0; i < propertyCount; i++) {
				// Leo el nombre de la propiedad
				byte[] nameBytes = new byte[in.readInt()];
				bytesRead = in.read(nameBytes);
				if (bytesRead != nameBytes.length) {
					throw new InvalidHeaderException("Fin inesperado de FileHeader serializado");
				}
				
				// Leo el valor de la propiedad
				byte[] valueBytes = new byte[in.readInt()];
				bytesRead = in.read(valueBytes);
				if (bytesRead != valueBytes.length) {
					throw new InvalidHeaderException("Fin inesperado de FileHeader serializado");
				}
				
				// Guardo la propiedad en el header
				String name = new String(nameBytes);
				String value = new String(valueBytes);
				header.setProperty(name, value);
			}
		} catch (IOException e) {
			throw new InvalidHeaderException("Error al deserializar FileHeader", e);
		}
		
		return header;
	}
	
	/**
	 * Calcula el tamanio serializado de una propiedad cuyo nombre y valor
	 * recibe por parametro
	 * @param name nombre de la propiedad
	 * @param value valor de la propiedad
	 * @return tamanio serializado de la propiedad name=value
	 */
	private int calculatePropertySize(String name, String value) {
		return 8 + name.length() + value.length();
	}
}
